/*******************************************************************************
 * Copyright (c) 2008-2010 Cedric Vidal
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *     Cedric Vidal - original idea, initial contribution and API
 *******************************************************************************/

package org.trans4j.impl;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Collections;

import net.sf.cglib.core.ReflectUtils;
import net.sf.cglib.core.Signature;
import net.sf.cglib.proxy.MethodProxy;

import org.trans4j.thirdparty.PolymorphicDispatcher;
import org.trans4j.thirdparty.PolymorphicDispatcher.ErrorHandler;

/**
 * @author <a href="mailto:c.vidal@proxiad.com">Cedric Vidal, ProxiAD Group</a>
 * 
 */
public class PolymorphicMapper extends AbstractDelegateMethodInterceptor {

	private CacheSupport<Integer, PolymorphicDispatcher<? extends Object>> cacheSupport = new CacheSupport<Integer, PolymorphicDispatcher<? extends Object>>();

	private Hasher hasher = new Hasher();

	public PolymorphicMapper() {
		super();
	}

	public final Object intercept(Object obj, Method method, Object[] args,
			MethodProxy proxy) throws Throwable {
		boolean topLevel = !cacheSupport.isCacheActive();
		Object object = null;
		try {
			if (!isSpecial(method)) {
				object = doInvoke(obj, method, args, proxy);
			}
		} finally {
			if (topLevel) {
				cacheSupport.releaseCache();
			}
		}
		return object;
	}

	private boolean isSpecial(Method method) {
		String name = method.getName();
		return "hashCode".equals(name) || "equals".equals(name);
	}

	private Object doInvoke(final Object enhanced, final Method method,
			Object[] args, final MethodProxy proxy) throws Throwable {
		PolymorphicDispatcher<? extends Object> dispatcher = null;

		cacheSupport.init();
		Integer hash = hasher.hash(method, enhanced);
		dispatcher = cacheSupport.cached(hash);

		if (dispatcher == null) {
			int paramCount = method.getParameterTypes().length;
			String methodName = method.getName();
			dispatcher = new PolymorphicDispatcher<Object>(methodName,
					paramCount, paramCount,
					Collections.singletonList(enhanced),
					new ErrorHandler<Object>() {
						public Object handle(Object[] params, Throwable e) {
							return handleError(method, params, e);
						}

					}) {

				@Override
				protected Object doInvoke(Method altMethod, Object target,
						Object... params) throws IllegalAccessException,
						InvocationTargetException {
					Signature altSignature = ReflectUtils
							.getSignature(altMethod);
					MethodProxy altProxy = MethodProxy.find(
							enhanced.getClass(), altSignature);
					try {
						return invokeDelegateOrSuper(target, altMethod, params,
								altProxy);
					} catch (Throwable e) {
						throw new InvocationTargetException(e);
					}
				}

			};
			cacheSupport.cache(hash, dispatcher);
		}

		Object result;
		try {
			result = dispatcher.invoke(args);
		} catch (WrappedException e) {
			throw e.getCause();
		}
		return result;
	}

	static class WrappedException extends RuntimeException {

		private static final long serialVersionUID = -5130386347474966361L;

		public WrappedException(Throwable cause) {
			super(cause);
		}

	}

	protected Object handleError(Method method, Object[] params, Throwable e) {
		throw new WrappedException(e);
	}

}
